package distributed_mutex

import (
	"context"
	"fmt"
	"github.com/go-errors/errors"
	"github.com/redis/go-redis/v9"
	"github.com/zeromicro/go-zero/core/logc"
	"golang.org/x/exp/rand"
	"payme3000/framework/errorx"
	"time"
)

const (
	KeyPrefix             = "dist:mx"
	MinRetryDelayMilliSec = 50
	MaxRetryDelayMilliSec = 250
)

type (
	DistributedMutex struct {
		name   string
		expiry time.Duration
		tries  uint8

		client redis.UniversalClient // 缓存驱动
	}

	UnLockFunc func(context.Context)
	OptionFunc func(*DistributedMutex)
)

func NewDistributedMutex(
	name string,
	client redis.UniversalClient,
	options ...OptionFunc,
) *DistributedMutex {
	m := &DistributedMutex{
		name:   name,
		expiry: 8 * time.Second,
		tries:  64,
		client: client,
	}

	for _, opt := range options {
		opt(m)
	}

	return m
}

// LockWithKey 锁定指定key
// @param ctx context.Context 用于退出持续尝试获取锁的等待
// @return UnLockFunc 调用后执行对应key的unlock
func (m *DistributedMutex) LockWithKey(ctx context.Context, key string) (UnLockFunc, error) {
	key = m.GetKey(key)
	value := time.Now().Unix()

	for i := uint8(0); i < m.tries; i++ {
		// 不是第一次时, 尝试获取锁时需要有时间间隔
		if i != 0 {
			select {
			case <-ctx.Done():
				// Exit early if the context is done.
				logc.Infof(ctx, "early exit to acquire lock wit key:%s", key)
				return nil, context.DeadlineExceeded
			case <-time.After(m.GetDelayDuration()):
				// Fall-through when the delay timer completes.
			}
		}

		// try to acquire lock
		ok, err := m.client.SetNX(
			ctx,
			key,
			value,
			m.expiry,
		).Result()
		if err != nil {
			if errors.Is(err, context.DeadlineExceeded) {
				logc.Infof(ctx, "early exit to acquire lock with err:%s, key:%s", err, key)
				return nil, err
			}

			return nil, errorx.Throw(ctx, err)
		}

		if ok {
			// lock成功 //

			// 设置版本号
			_ver := value
			return func(unlockCtx context.Context) {
				if _ver == value {
					if delErr := m.client.Del(unlockCtx, key).Err(); delErr != nil {
						_ = errorx.Errorf(unlockCtx, "unlock error with err:%s, key:%s", delErr, key)
					}

					_ver++
				}
			}, nil
		}

		// lock失败
		// 持续尝试获取锁
	}

	return nil, errorx.Errorf(ctx, "failed to acquire lock with key:%s", key)
}

func (m *DistributedMutex) GetKey(key string) string {
	return fmt.Sprintf("%s:%s:%s", KeyPrefix, m.name, key)
}

// GetDelayDuration 持续尝试获每次取锁时的时间间隔, 默认是50到250毫秒之间
func (m *DistributedMutex) GetDelayDuration() time.Duration {
	return time.Duration(rand.Intn(MaxRetryDelayMilliSec-MinRetryDelayMilliSec)+MinRetryDelayMilliSec) * time.Millisecond
}
